Ordinary
About

만들면서 배우는 클린 아키텍처 (1 ~ 3)

profileordilov / 2022. 3. 3

학습 목표

  • 계층형 아키텍처의 잠재적인 단점 파악
  • 아키텍처 경계를 강제하는 방법들
  • 잠재적인 지름길들이 소프트웨어 아키텍처에 끼치는 영향
  • 언제 어떤 스타일의 아키텍처를 사용할지 결정
  • 아키텍처에 따른 코드 구성
  • 아키텍처의 각 요소를 포함하는 테스트 적용

계층형 아키텍처의 문제는 무엇일까?

1.1

일반적인 3계층 아키텍처는 웹 계층에서 요청을 받아 도메인 혹은 비즈니스 계층에 있는 서비스로 요청을 보냅니다. 서비스에서는 비즈니스 로직을 수행하고, 도메인 엔티티의 상태 조회나 변경을 위해 영속성 계층의 컴포넌트를 호출합니다.

이런 계층형 아키텍처의 문제점은 코드에 나쁜 습관이 스며들기 쉽고 시간이 지날수록 변경하기 어려워지는 허점이 있습니다.

계층형 아키텍처는 데이터베이스 주도 설계를 유도한다.

전통적인 계층형 아키텍처의 토대는 데이터베이스입니다. 그림 1.1처럼 영속성 계층을 가장 기본으로 의존하기 떄문에 모든 것은 영속성 기준으로 만들어집니다.

우리가 만드는 애플리케이션의 목적은 규칙이나 정책을 반영한 모델을 만들어 규칙과 정책을 편리하게 활용하게 만듭니다. 이때 우리는 상태가 아니라 행동을 중심으로 모델링하게 됩니다. 행동을 통해 상태를 바꿔나갑니다.

계층형 아키텍처로 구성할 때를 생각해보면 도메인 로직보다 데이터베이스 구조를 먼저 생각하게 됩니다. 이는 합리적인 것이 의존성의 방향에 따라 자연스럽게 구현한 것이기 때문입니다. 하지만 비즈니스적 관점에서는 다른 무엇보다도 도메인 로직을 먼저 만들어야 합니다. 그래야 우리가 로직을 제대로 이해했는지 확인할 수 있고 그를 기반으로 영속성과 웹 계층을 만들어야 합니다.

데이터베이스 중심적인 아키텍처가 만들어지는 가장 큰 원인은 ORM 프레임워크를 사용하기 때문입니다. ORM 프레임워크를 사용하면서 비즈니스 규칙과 영속성 관점을 섞고 싶은 유혹을 쉽게 받게 됩니다. 이렇게 되면 서비스는 영속성 모델을 비즈니스 모델처럼 사요앟게 되고 도메인로직과 영속성 작업을 같이 하게 됩니다.

지름길을 택하기 쉬워진다.

전통적인 계층형 아키텍처에서의 전체적으로 적용되는 유일한 규칙은, 특정 계층은 같거나 아래 계층만 접근 가능합니다. 상위 계층에 위치한 컴포넌트에 접근해야 한다면 컴포넌트를 아래 계층으로 내려야 합니다. 하지만 이렇게 컴포넌트를 내리다보면 영속성 계층이 비대해지게 됩니다. 기본적으로 리포지토리를 포함해 헬퍼, 유틸리티 클래스등이 포함되 점점 커지게 됩니다. 이런 지름길을 멈추려면 추가적인 아키텍처 규칙을 강제로 만드는 것이 최선입니다.

테스트하기 어려워진다.

웹 계층에서 간단한 조작의 경우 영속성 계층에 바로 접근하다보면 처음엔 편하더라도 나중에 확장성이 떨어지게 됩니다. 테스트를 할 때도 웹 계층인데도 영속성 계층도 함께 모킹이 필요하게 되어 단위 테스트의 복잡도가 올라갑니다.

유즈케이스를 숨긴다.

개발자들은 새로운 유즈케이스를 구현하는 새로운 코드를 짜는 것을 선호합니다. 레거시 프로젝트를 리팩토링하거나 하는 일은 새로운 코드보다 더 오랜 시간이 걸립니다. 이럴 때 계층형 아키텍처에서 한 서비스의 너비가 크게 확장된 서비스가 만들어지기도 합니다. 이 경우 서비스는 영속성 계층에 많은 의존성을 갖고, 많은 웹 레이어가 이 서비스에 의존하게 됩니다. 서비스를 테스트하기도 어려워지고 작업해야할 유즈케이스를 책임지는 서비스를 찾기도 어려워집니다.

동시 작업이 어려워진다.

계층형 구조에서 작업하는 사람이 늘어난다고 해서 효율이 극대화되지 않습니다. 이유는 모든 것이 영속성 계층 위에 만들어지기 때문에 영속성 -> 도메인 -> 웹 순서로 만들게 됩니다.
개발자끼리 한 기능에 대해 역할을 분담하기 힘들어집니다. 특히 한 서비스에서 서로 다른 기능을 만들 때도 같은 파일을 편집하고 있어 병합 충돌이 날 가능성이 높습니다.

의존성 역전하기

단일 책임 원칙

하나의 컴포넌트는 오로지 한 가지 일만 해야 하고, 그것을 올바르게 수행해야 한다.

컴포넌트를 변경하는 이유는 오직 하나 뿐이어야 한다.

단일 책임 원칙의 실제 정의는 후자에 가까우며 다른 이유로 바뀐다면 컴포넌트가 영향받지 않는 걸 의미합니다.

의존성 역전 원칙

계층형 아키텍처에서 계층 간 의존성은 항상 다음 아래 계층을 가리킵니다. 따라서 영속성 계층을 변경하면 그 위의 계층인 도메인 계층도 변경하게 됩니다. 도메인 계층을 영속성 계층의 변경에 영향을 안 받으려면 의존성 역전 원칙을 사용하면 됩니다.

코드상의 어떤 의존성이든 그 방향을 바꿀 수 있다.

먼저 엔티티를 도메인 계층으로 옮기고 리포지토리를 인터페이스로 만들어 도메인으로 옮깁니다. 이전의 ORM 구현체는 영속성 계층에 두고 도메인의 인터페이스를 구현하게 만듭니다.

클린 아키텍처

엔티티를 중심으로 유즈 케이스가 위치하는 아키텍처입니다. 유즈케이스는 서비스와 비슷하지만 단일 책임을 위해 세분화되어있다는 차이점이 있습니다. 따라서 서비스가 넓어지는 문제를 피할 수 있습니다.

육각형 아키텍처

육각형 안에는 도메인 엔티티와 이와 상호작용하는 유스케이스가 있습니다. 육각형 바깥에는 애플리케이션과 상호 작용하는 웹, 데이터베이스 같은 어댑터들이 존재합니다. 어댑터가 코어를 호출한다면 입력 포트로, 코어에서 호출된다면 출력 포트와 연결됩니다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

가장 중요한 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 되어 코드를 변경할 이유의 수를 줄여줍니다.

코드 구성하기

어떤 아키텍처인지 파악하기 쉽게 구성하는 방법은 패키지 구조로 만들 수 있습니다.

계층으로 구성하기

web, domain, persistence 상위 패키지 아래에 각 요소들을 구성하는 방식입니다. 이런 구조의 문제점은 기능에 따라 구분하기 어렵습니다. 하나의 기능을 추가하기 위해선 web, domain, persistence 모든 패키지에 각각 필요한 기능들을 추가해야 합니다. 다음으로 어떤 유즈케이스를 제공하는지 알기 힘듭니다. 특정 기능에 대한 구현을 어떤 도메인이 했겠지라고 추측하고 찾아봐야 합니다.

기능으로 구성하기

각 도메인 별로 구성하는 방법입니다. account 도메인이라면 한 패키지 안에 acocunt, accountRepository등이 들어가게 됩니다. 이렇게 구성하게 되면 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있습니다. 하지만 이러한 구성은 각 어댑터나 포트 같은 역할에 따른 구성을 파악하기 힘듭니다.

아키텍처적으로 표현력 있는 패키지 구조

각 도메인 밑에 adapter, domain, application 등 아키텍처의 요소들을 매핑시킵니다. 경계선이 되는 도메인 밑에 필요한 요소들을 명시적으로 구분한 구조로 코드가 아키텍처를 그대로 반영하게 됩니다.